Skip to content

NAS backup: compression, encryption, bandwidth throttle, integrity check - #12898

Open
jmsperu wants to merge 18 commits into
apache:4.22from
jmsperu:fix/nasbackup-enhancements-combined
Open

jmsperu wants to merge 18 commits into
apache:4.22from
jmsperu:fix/nasbackup-enhancements-combined

Conversation

@jmsperu

@jmsperu jmsperu commented Mar 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds four optional, zone-scoped features to NAS backup operations on KVM, all disabled by default:

  • Compression (-c): Uses qcow2 internal compression (qemu-img convert -c) to reduce backup size
  • LUKS Encryption (-e): Encrypts backup files at rest using LUKS via qemu-img convert --object secret
  • Bandwidth Throttle (-b): Limits backup I/O — virsh blockjob --bandwidth for running VMs, qemu-img convert -r + ionice for stopped VMs
  • Integrity Check (--verify): Runs qemu-img check on each backup file after creation

Configuration Keys (Zone scope)

Setting Type Default Description
nas.backup.compression.enabled Boolean false Enable qcow2 compression for backup files
nas.backup.encryption.enabled Boolean false Enable LUKS encryption for backup files
nas.backup.encryption.passphrase String (Secure) "" Passphrase for LUKS encryption
nas.backup.bandwidth.limit.mbps Integer 0 Bandwidth limit in MiB/s (0 = unlimited)
nas.backup.integrity.check Boolean false Run qemu-img check after backup

Architecture

  1. NASBackupProvider reads zone-scoped ConfigKeys and populates a details map on TakeBackupCommand
  2. TakeBackupCommand carries the details map from management server to KVM agent
  3. LibvirtTakeBackupCommandWrapper extracts the details and translates them to nasbackup.sh CLI flags
  4. nasbackup.sh implements the actual compression, encryption, throttling, and verification logic

Files Changed

  • scripts/vm/hypervisor/kvm/nasbackup.sh — new -c, -b, -e, --verify flags with encrypt_backup() and verify_backup() functions
  • core/.../TakeBackupCommand.java — added details map (HashMap) with getter/setter/addDetail
  • plugins/backup/nas/.../NASBackupProvider.java — 5 new ConfigKeys, populate command details in takeBackup()
  • plugins/hypervisors/kvm/.../LibvirtTakeBackupCommandWrapper.java — extract details, build dynamic CLI args, temp passphrase file lifecycle

Notes

Test plan

  • Verify backup works with all four features disabled (default) — no behavioral change
  • Enable nas.backup.compression.enabled at zone scope, take backup, verify qcow2 files are compressed
  • Enable nas.backup.bandwidth.limit.mbps (e.g. 50), take backup of running VM, verify virsh blockjob bandwidth is applied
  • Enable nas.backup.bandwidth.limit.mbps, take backup of stopped VM, verify qemu-img -r rate limit is applied
  • Enable nas.backup.encryption.enabled with passphrase, take backup, verify files are LUKS encrypted (qemu-img info shows encryption)
  • Enable nas.backup.integrity.check, take backup, verify qemu-img check runs and passes
  • Test with multiple features enabled simultaneously (compression + integrity check)
  • Verify restore still works for backups created with compression/encryption
  • Test with RBD storage pools — verify bandwidth throttle applies correctly

… integrity check

Adds four optional features to NAS backup operations, configurable at
zone scope via CloudStack global settings:

- Compression (-c): qcow2 internal compression of backup files
  Config: nas.backup.compression.enabled (default: false)

- LUKS Encryption (-e): encrypt backup files at rest using qemu-img
  Config: nas.backup.encryption.enabled (default: false)
  Config: nas.backup.encryption.passphrase (Secure category)

- Bandwidth Throttle (-b): limit backup I/O bandwidth via virsh
  blockjob for running VMs or qemu-img -r for stopped VMs
  Config: nas.backup.bandwidth.limit.mbps (default: 0/unlimited)

- Integrity Check (--verify): qemu-img check after backup creation
  Config: nas.backup.integrity.check (default: false)

All features are disabled by default and fully backward compatible.
Settings are read from zone-scoped ConfigKeys in NASBackupProvider,
passed to the KVM agent via TakeBackupCommand details map, and
translated to nasbackup.sh CLI flags in LibvirtTakeBackupCommandWrapper.

Changes:
- nasbackup.sh: add -c, -b, -e, --verify flags with encrypt_backup()
  and verify_backup() helper functions
- TakeBackupCommand.java: add details map for passing config to agent
- NASBackupProvider.java: add 5 ConfigKeys, populate command details
- LibvirtTakeBackupCommandWrapper.java: extract details, build CLI args,
  handle passphrase temp file lifecycle

Combines and supersedes PRs apache#12844, apache#12846, apache#12848, apache#12845
@codecov

codecov Bot commented Mar 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.10870% with 55 lines in your changes missing coverage. Please review.
✅ Project coverage is 18.02%. Comparing base (f23766d) to head (d32f570).
⚠️ Report is 12 commits behind head on 4.22.

Files with missing lines Patch % Lines
...ce/wrapper/LibvirtRestoreBackupCommandWrapper.java 70.31% 15 Missing and 4 partials ⚠️
...ource/wrapper/LibvirtTakeBackupCommandWrapper.java 63.15% 13 Missing and 1 partial ⚠️
.../kvm/resource/wrapper/NasBackupPassphraseFile.java 62.50% 6 Missing and 3 partials ⚠️
...rg/apache/cloudstack/backup/TakeBackupCommand.java 40.00% 6 Missing ⚠️
...rg/apache/cloudstack/backup/NASBackupProvider.java 90.47% 0 Missing and 4 partials ⚠️
...apache/cloudstack/backup/RestoreBackupCommand.java 50.00% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               4.22   #12898      +/-   ##
============================================
+ Coverage     17.99%   18.02%   +0.02%     
- Complexity    16215    16278      +63     
============================================
  Files          5935     5937       +2     
  Lines        535706   535937     +231     
  Branches      65596    65629      +33     
============================================
+ Hits          96425    96622     +197     
- Misses       428292   428317      +25     
- Partials      10989    10998       +9     
Flag Coverage Δ
uitests 4.04% <ø> (+0.02%) ⬆️
unittests 19.10% <70.10%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds optional, zone-scoped enhancements for KVM NAS backups (compression, LUKS encryption, bandwidth throttling, and post-backup integrity verification) by plumbing config from management server → TakeBackupCommand details → KVM agent wrapper → nasbackup.sh flags.

Changes:

  • Add new CLI flags and implementation in nasbackup.sh for compression (-c), encryption (-e), bandwidth throttling (-b), and verification (--verify).
  • Extend TakeBackupCommand with a details map to carry optional settings to the agent.
  • Add zone-scoped NAS backup ConfigKeys and populate command details; update KVM wrapper to translate details into script args and manage a temporary passphrase file.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.

File Description
scripts/vm/hypervisor/kvm/nasbackup.sh Implements compression/encryption/throttle/verify logic and argument parsing for NAS backup operations.
core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java Adds a details map to carry optional backup feature settings from management to agent.
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java Introduces zone-scoped ConfigKeys and passes enabled settings into TakeBackupCommand details.
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java Builds dynamic nasbackup.sh command args from TakeBackupCommand details and writes an encryption passphrase temp file.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh
Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh
Comment thread core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java Outdated
Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh
Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh Outdated
- nasbackup.sh: Replace exit 1 with return 1 in encrypt_backup and
  verify_backup so callers can run cleanup before terminating
- nasbackup.sh: Append (>>) instead of truncate (>) agent.log in
  qemu-img convert for stopped VM backups
- nasbackup.sh: Add return 1 after cleanup on qemu-img convert failure
  to stop execution
- nasbackup.sh: Callers of encrypt_backup/verify_backup now check
  return code and run cleanup on failure
- LibvirtTakeBackupCommandWrapper: Fail with error when encryption is
  enabled but passphrase is missing instead of silently skipping
- LibvirtTakeBackupCommandWrapper: Delete temp passphrase file in
  finally block, set 0600 permissions, use explicit UTF-8 charset
- NASBackupProvider: Throw CloudRuntimeException when encryption is
  enabled but passphrase is null/empty
- NASBackupProviderTest: Add tests for compression, bandwidth,
  integrity check, encryption+passphrase, and encryption-without-
  passphrase failure scenarios
- TakeBackupCommand: Add @loglevel(Off) to details field to prevent
  passphrase leaking in debug logs
- TakeBackupCommand: Normalize null to empty HashMap in setDetails
@sureshanaparti

Copy link
Copy Markdown
Contributor

@blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@sureshanaparti a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 17323

@sureshanaparti

sureshanaparti commented Apr 1, 2026 •

Copy link
Copy Markdown
Contributor

@jmsperu can you check/fix the build failure.

jmsperu added 2 commits April 2, 2026 00:45
Address remaining Copilot review feedback on PR apache#12898:
- Replace `2>&1 | tee -a` with `>> logFile 2>&1` in encrypt_backup,
  compress, and mount_operation to prevent tee from masking non-zero
  exit codes of qemu-img and mount commands
- Add `return 1` after cleanup on virsh backup job failure to prevent
  continuing execution with broken state
The test helper overrideConfigValue() was only setting _value on
ConfigKey, but zone-scoped configs (valueIn(zoneId)) fall back to
_defaultValue when s_depot is null in test context. Also set
_defaultValue via ReflectionTestUtils to ensure valueIn() returns
the expected test value.

Fixes: 4 assertion failures (compression, bandwidth, encryption,
integrity_check details all returned null) and 1 error
(encryption without passphrase expected CloudRuntimeException
but got NullPointerException from null config value).
@jmsperu

jmsperu commented Apr 2, 2026

Copy link
Copy Markdown
Collaborator Author

@sureshanaparti Fixed. The test failures were caused by overrideConfigValue() in NASBackupProviderTest only setting _value on ConfigKey, but the zone-scoped configs (valueIn(zoneId)) fall back to _defaultValue when s_depot is null in the test context. All 5 config values (compression, bandwidth, encryption, encryption passphrase, integrity check) were returning null instead of the test values.

The fix also sets _defaultValue via ReflectionTestUtils so valueIn() correctly resolves test values.

Also addressed in the previous commit: replaced 2>&1 | tee -a with >> logFile 2>&1 in nasbackup.sh to prevent tee from masking non-zero exit codes, and added return 1 after cleanup on virsh backup job failure.

Could you please retrigger the build? @blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@jmsperu a [SL] Jenkins job has been kicked to build packages. It will be bundled with` SystemVM template(s). I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 17346

…ments-combined

Signed-off-by: James Peru <jmsperu@gmail.com>

# Conflicts:
#	plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java
#	plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java
@jmsperu

jmsperu commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current 4.22 (merge 7f2c57ab1a) — this is mergeable again.

Two files conflicted, both from the recent command-injection hardening on 4.22:

  • LibvirtRestoreBackupCommandWrapper — 4.22 reordered the imports and dropped the shell-string constants (MOUNT_COMMAND, UMOUNT_COMMAND, ATTACH_*, CURRRENT_DEVICE, RSYNC_COMMAND) in favour of argv-array Script.executeCommand(...). I took 4.22's structure wholesale and re-applied the LUKS work on top, so the non-encrypted path is now upstream's hardened rsync argv form and only the encrypted path builds a qemu-img convert. The one constant this PR still needs, LUKS_SECRET_ID, is kept.
  • LibvirtRestoreBackupCommandWrapperTest — import-ordering only.

No behaviour from either side was dropped: the hardening applies to the plain path, the decrypt-on-restore applies to the encrypted path, and they do not overlap.

Verified locally on JDK17:

  • LibvirtRestoreBackupCommandWrapperTest + LibvirtTakeBackupCommandWrapperTest — 18/18 pass
  • NASBackupProviderTest — 15/15 pass
  • bash -n nasbackup.sh clean

Ready for @blueorangutan package / test whenever a committer can approve-and-run.

@blueorangutan

Copy link
Copy Markdown

@jmsperu a [SL] Jenkins job has been kicked to build packages. It will be bundled with /test` whenever a committer can approve-and-run. SystemVM template(s). I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 19011

@DaanHoogland DaanHoogland moved this from Backlog to conflict/waiting for author in CloudStack Testing Aug 31, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.

…ments-combined

Signed-off-by: James Peru <jmsperu@gmail.com>

# Conflicts:
#	plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java
@jmsperu

jmsperu commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Synced with 4.22 again (a39a95d). One conflict, in LibvirtRestoreBackupCommandWrapperTest: both sides added new test methods in the same place (the LUKS restore tests here, the attach-device tests from #14007 upstream), so both are kept. The wrapper itself merged cleanly and now carries upstream's restore-and-attach fix alongside the encrypted-restore path.

Verified on JDK 17: LibvirtRestoreBackupCommandWrapperTest 19/19, LibvirtTakeBackupCommandWrapperTest 5/5, NASBackupProviderTest 15/15, bash -n nasbackup.sh clean. Ready for a package and test run.

@DaanHoogland DaanHoogland modified the milestones: 24.0, 4.22.2 Sep 9, 2026
@DaanHoogland DaanHoogland moved this from conflict/waiting to Ready in CloudStack Testing Sep 9, 2026
@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 19185

@abh1sar abh1sar moved this from Ready to In progress in CloudStack Testing Sep 10, 2026

@abh1sar abh1sar left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the encryption passphrase be rotated/change? if yes, then older backups will become unrecoverable.

Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh Outdated
Comment on lines +107 to +114
local compress_flag=""
if [[ "$COMPRESS" == "true" ]]; then
compress_flag="-c"
fi
for img in "$backup_dir"/*.qcow2; do
[[ -f "$img" ]] || continue
local tmp_img="${img}.luks"
if qemu-img convert $compress_flag -O qcow2 \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: qcow2 cannot compress and encrypt at the same time, so enabling both nas.backup.compression.enabled and nas.backup.encryption.enabled fails every backup and then deletes it.

Reproduced with qemu-img 9.2.3:

$ qemu-img convert -c -O qcow2 --object secret,id=sec0,file=pass.key \
    -o encrypt.format=luks,encrypt.key-secret=sec0 plain.qcow2 out.qcow2
qemu-img: Compression and encryption not supported at the same time

With both settings on, encrypt_backup returns 1, the caller runs cleanup, and the whole backup directory is removed. This hits both the running-VM and stopped-VM paths.

The comment above compress_flag says it exists so compression is not silently discarded, but the result is a hard failure instead. Could you either reject the combination up front in applyBackupEnhancementDetails (clearest, the admin gets told why) or drop -c here and document that encryption wins? Either way it would be good to add a test for the two together, since neither the unit tests nor the test-plan checklist covers that combination today.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and thanks for the reproduction. I got the same error with qemu-img 11.1.2, and the old script built the backup and then deleted it. I went with rejecting the combination:

  • f82d33f: applyBackupEnhancementDetails fails the request before anything is created when both settings are on for the zone. The error names both settings and says no backup was taken, and both setting descriptions now say they cannot be combined.
  • 1f596ed: nasbackup.sh refuses -c together with -e before it mounts anything (defence in depth for direct calls), and encrypt_backup no longer passes -c.

Tests: testTakeBackupRejectsCompressionWithEncryptionBeforeCreatingBackup (no persist, no agent call), plus a script run with -c -e against real qemu-img: it now exits 1 with the message and never mounts. Without the fix, the backup was created and then deleted.

command.setQuiesce(quiesceVM);

// Pass optional backup enhancement settings from zone-scoped configs
applyBackupEnhancementDetails(command, vm.getDataCenterId());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: createBackupObject() on line 242 has already persisted a BackupVO in BackingUp state by the time this runs, so when encryption is enabled without a passphrase the CloudRuntimeException thrown from applyBackupEnhancementDetails leaves an orphaned backup row stuck in BackingUp forever.

Every other failure path in takeBackup calls backupDao.remove(backupVO.getId()) before throwing. Since the validation needs nothing from the backup object, moving this call above line 242 fixes it cleanly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, thanks. Fixed in f82d33f: the command is built and applyBackupEnhancementDetails runs before createBackupObject, so a missing passphrase (or the compression + encryption combination) fails before any row exists. testTakeBackupEncryptionWithoutPassphraseThrows now verifies that backupDao.persist and the agent are never called. Mockito's strict stubs also flagged that the volume lookup no longer runs, which confirms the new order.

Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh Outdated
fi
output="$dest/$name.$volUuid.qcow2"
if ! qemu-img convert -O qcow2 "$disk" "$output" > "$logFile" 2> >(cat >&2); then
if ! ionice -c 3 qemu-img convert $([[ "$COMPRESS" == "true" ]] && echo "-c") $([[ -n "$BANDWIDTH" ]] && echo "-r" "${BANDWIDTH}M") -O qcow2 "$disk" "$output" >> "$logFile" 2> >(cat >&2); then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: qemu-img convert -r is not available on the minimum QEMU this script supports. sanity_checks() only requires QEMU >= 4.2.0, but -r rate_limit was added much later (it is present in 9.2, absent in 4.2). On a 4.2/5.x host (RHEL 8, Ubuntu 20.04) enabling nas.backup.bandwidth.limit.mbps would make every stopped-VM backup fail with invalid option -- 'r'.

Could you check the exact QEMU version that introduced it and either probe for support with a graceful fallback, or raise the minimum version check when bandwidth limiting is requested?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. -r was added in QEMU 5.2.0 (qemu commit 0c8c4895a6, "qemu-img: add support for rate limit in qemu-img convert"), so 4.2 and 5.0/5.1 reject it. In 1f596ed the script probes for it by running qemu-img convert -r on a throwaway 1 MiB image, rather than parsing --help, whose format has changed between releases. If the probe fails, the backup still runs at idle I/O priority but without -r, and the agent log gets a WARNING saying the limit could not be applied and that QEMU >= 5.2 is required. I chose not to fail the backup in that case, because a missing backup is worse than an unthrottled one.

I checked both paths with the PATH-shimmed harness against real qemu-img and a shim that rejects -r like old QEMU. Before the fix, the backup failed with invalid option -- 'r'. After it, the backup succeeds and the warning is logged. While doing this I also found that the old $(...) substitution passed -r 50M as a single word, because IFS=, is set in that function. The convert arguments are now built as arrays.

Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh Outdated
fi
output="$dest/$name.$volUuid.qcow2"
if ! qemu-img convert -O qcow2 "$disk" "$output" > "$logFile" 2> >(cat >&2); then
if ! ionice -c 3 qemu-img convert $([[ "$COMPRESS" == "true" ]] && echo "-c") $([[ -n "$BANDWIDTH" ]] && echo "-r" "${BANDWIDTH}M") -O qcow2 "$disk" "$output" >> "$logFile" 2> >(cat >&2); then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ionice -c 3 is applied unconditionally here, so it also affects backups where none of the four new settings are enabled. That is a silent default behaviour change for existing users. Could it be gated on $BANDWIDTH being set?

Worth noting too that -c 3 (idle) only has an effect under CFQ/BFQ. With mq-deadline or none, the default for NVMe on current kernels, it is a no-op, so it may not buy much even when the feature is on.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that was an unintended default change. In 1f596ed ionice -c 3 is only added when a bandwidth limit is set, so backups without the new settings run the exact command they ran before (I checked the harness call log for both cases). You are also right that the idle class does nothing under mq-deadline or none. It is now only a best-effort extra next to -r, and on hosts without -r the warning says the backup ran "at idle I/O priority only" rather than claiming it was throttled.

Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh Outdated
Comment on lines +253 to +257
if [[ -n "$BANDWIDTH" ]]; then
for disk in $(virsh -c qemu:///system domblklist $VM --details 2>/dev/null | awk '/disk/{print$3}'); do
virsh -c qemu:///system blockjob $VM $disk --bandwidth "${BANDWIDTH}" 2>/dev/null || true
done
log -ne "Backup bandwidth limited to ${BANDWIDTH} MiB/s per disk for $VM"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things here.

  1. stderr is sent to /dev/null and the failure is swallowed by || true, but line 257 then logs that the limit was applied regardless. If the call fails, the admin sees a log line claiming throttling is active while nothing is throttled. Could you capture the exit status and log the actual outcome?

  2. I am not sure libvirt accepts blockjob --bandwidth against a push-mode backup job. Backups are reported through domjobinfo rather than blockjob, so the lookup may not resolve. Could you confirm on your target libvirt version that the bandwidth is actually applied, rather than the command erroring out silently?

Minor: awk '/disk/{print$3}' matches any line containing "disk", including a cdrom row whose source path happens to contain it. The existing code a few lines below uses awk '$2=="disk"', which would be more robust and consistent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Fixed in 1f596ed. Each blockjob --bandwidth call's exit status and stderr are captured. A disk that could not be throttled gets a WARNING with libvirt's error, and the summary line says how many disks were throttled and how many were not, so the log no longer claims a limit that is not in place.

  2. Fair question. From reading libvirt's qemu driver, backup-begin registers a block job on each disk, which is what virDomainBlockJobSetSpeed looks up, so I expect it to resolve. I have not confirmed that on a real host with the current change yet. I will test it on a lab host and report back here. If it does not work there, I will drop the running-VM throttle and document the limit as stopped-VM only, rather than keep something that is a no-op.

Minor: done. It now uses awk '$2=="disk"{print $3}'. The harness showed the old pattern also hit a cdrom row whose ISO path contained "disk".

Comment thread scripts/vm/hypervisor/kvm/nasbackup.sh Outdated
Comment on lines +141 to +153
local check_ok=0
if [[ ${#check_secret[@]} -gt 0 ]]; then
qemu-img check "${check_secret[@]}" --image-opts \
"driver=qcow2,file.filename=$img,encrypt.key-secret=sec0" \
> /dev/null 2>&1 && check_ok=1
else
qemu-img check "$img" > /dev/null 2>&1 && check_ok=1
fi
if [[ $check_ok -eq 1 ]]; then
log -ne "Backup verification passed: $img"
else
echo "Backup verification failed for $img"
log -ne "Backup verification FAILED: $img"

@abh1sar abh1sar Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only exit code 0 is accepted, and a non-zero result makes the caller run cleanup, deleting the entire backup.

qemu-img check uses 2 for a corrupt image and 3 for leaked clusters. Leaks are benign, so as written a backup with leaked clusters is declared failed and destroyed. Could you confirm the exit code semantics on your side and treat 3 as a warning rather than a failure?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, you are right: 0 is clean, 2 is corruption, 3 is leaked clusters only, 1 means the check could not complete, and 63 means the format cannot be checked. In 1f596ed verify_backup accepts 0, accepts 3 with a log line ("passed with leaked clusters (wasted space only, data intact)"), and fails on everything else with the exit code in the message.

I tested this with real qemu-img by editing the refcount table of a backup image: one with a leaked cluster (qemu-img check exits 3) and one with a refcount that is too low (exits 2). Plain and LUKS-encrypted images were both covered. Before the fix, the leaked image was declared failed and deleted. Now it is kept with the log line, and the corrupt one is still rejected. d32f570 applies the same rule on the restore side, so a backup with leaked clusters can be restored.

Comment on lines +283 to +286
private boolean isEncryptedImage(String backupPath) {
String info = Script.executeCommand("qemu-img", "info", "--output=json", backupPath);
return info != null && info.replaceAll("\\s", "").contains("\"encrypted\":true");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails open. If Script.executeCommand returns null (qemu-img missing, path unreadable, timeout) the method returns false, and replaceVolumeWithBackup then falls through to rsync, copying an encrypted qcow2 verbatim onto the volume. The restore reports success but the volume is unbootable.

Could you distinguish "not encrypted" from "could not determine" and fail loudly on the latter?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that was fail-open. Fixed in d32f570: isEncryptedImage now throws when qemu-img info returns nothing usable (missing binary, unreadable file, timeout), and the restore fails with "... not known whether it is encrypted; refusing to restore it" before any copy. The "not encrypted" result is only returned when qemu-img actually read the header and it has no "encrypted": true. The new test testRestoreFailsClosedWhenEncryptionStateCannotBeDetermined checks that neither rsync nor qemu-img convert runs in that case. Two existing tests had been passing only because their mocked qemu-img info returned null; they now return a real plain-qcow2 result.

@abh1sar abh1sar moved this from In progress to conflict/waiting in CloudStack Testing Sep 11, 2026
@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.

Resolves conflicts with the incremental NAS backup backport (apache#13877) and the
restore mount/unmount timeout fix (apache#14006). Both sets of zone settings and
command fields are kept; the agent wrappers carry both the mount timeout and
the LUKS key file, and the enhancement flags plus temporary passphrase file
move into upstream's new runBackupScript.

Compression and LUKS encryption rewrite each backup file with qemu-img
convert, which would flatten an incremental through its backing chain (or
leave an encrypted child whose parent needs its own secret). So a zone with
either enabled now takes full backups (decideChain returns legacy-full), and
nasbackup.sh refuses incremental mode combined with -c/-e as a backstop.
Covered by decideChainReturnsLegacyFullWhenCompressionOrEncryptionEnabled.

Claude-Session: https://claude.ai/code/session_011MtPDN3Yg3DR8wu4EN8nkp
Since apache#14006 mount/umount also go through Script.executeCommandForExitValue,
so the LUKS restore tests' call capture picked them up (4 calls, not 2).
Filter the capture to qemu-img command lines; the no-passphrase test now
asserts no qemu-img command ran instead of no command at all.

Claude-Session: https://claude.ai/code/session_011MtPDN3Yg3DR8wu4EN8nkp
@jmsperu

jmsperu commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Synced with 4.22 after #13877 and #14006 landed (merge 2a9bbbb). Both sets of settings are kept and the enhancement flags now live in the new runBackupScript.

One behavioural point for review: compression and LUKS encryption rewrite each backup file with qemu-img convert, which would flatten an incremental through its backing chain (or leave an encrypted child whose parent needs its own secret). So when nas.backup.compression.enabled or nas.backup.encryption.enabled is on for a zone, decideChain returns a full backup, and nasbackup.sh rejects -M incremental combined with -c/-e as a backstop (test: decideChainReturnsLegacyFullWhenCompressionOrEncryptionEnabled). Making both features chain-aware can follow separately if there's interest.

Local results (JDK 17): NASBackupProviderTest 28/28, Restore wrapper 22/22, Take wrapper 5/5.

@DaanHoogland

Copy link
Copy Markdown
Contributor

@abh1sar can you have another look?

…he backup row exists, reject compression with encryption

applyBackupEnhancementDetails now runs before createBackupObject, so encryption
without a passphrase no longer leaves an orphaned BackupVO in BackingUp. qcow2
cannot compress and encrypt the same image, so enabling both settings for a zone
fails the request up front with a message naming both settings, instead of
building the backup and deleting it. The setting descriptions say so, and the
passphrase description warns that restores use the current passphrase.
…ard, rate-limit probe, honest throttle logging, accept leaked clusters

- refuse -c together with -e before mounting anything (qcow2 cannot do both)
- qemu-img convert -r only exists since QEMU 5.2: probe it on a throwaway image
  and fall back to an unthrottled copy with a WARNING in the log
- ionice -c 3 only when a bandwidth limit is set; build the convert arguments as
  arrays (the old command substitution passed '-r 50M' as one word under IFS=,)
- running VMs: log per disk whether blockjob --bandwidth succeeded, and match
  disks with $2=="disk" so cdrom rows are skipped
- --verify: qemu-img check exit 3 (leaked clusters) passes with a log line;
  2 (corruption) and other codes still fail
…on state cannot be read

isEncryptedImage() now throws when qemu-img info returns nothing usable, so a
restore never falls back to rsync for a file that might be LUKS-encrypted.
qemu-img check exit 3 (leaked clusters) is accepted on restore as well; 2 still
reports the backup as corrupt. Tests cover both, and the existing tests now stub
a real plain-qcow2 qemu-img info result instead of relying on a null.
@jmsperu

jmsperu commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

@abh1sar thanks for the thorough review. All seven inline points are answered in their threads; the fixes are in f82d33f (management server), 1f596ed (nasbackup.sh) and d32f570 (restore wrapper). Local results: NASBackupProviderTest 29/29, LibvirtRestoreBackupCommandWrapperTest 24/24, LibvirtTakeBackupCommandWrapperTest 5/5, checkstyle clean. I also ran the script changes through a PATH-shimmed harness with real qemu-img, comparing old and new, for compression + encryption, -r with and without support, ionice gating, and qemu-img check exit codes 0, 2 and 3. One item is still open: confirming on a real host that blockjob --bandwidth applies to a push-mode backup job (see that thread).

On your question about rotating the passphrase: yes, today a restore only gets the zone's current nas.backup.encryption.passphrase, so changing it makes backups taken with the old one unrestorable until the old value is set back. You were right to raise it. For now the setting's description says so explicitly (f82d33f), and a restore that cannot open the image fails rather than copying it. Proper rotation would mean keeping the passphrase per backup (for example, a reference stored in backup details that keeps old passphrases readable). I would rather do that as a follow-up than grow this PR. Would that work for you?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: conflict/waiting

Development

Successfully merging this pull request may close these issues.

9 participants